﻿using System;
using Birkelund.QueryStrings.Tokens;
using NUnit.Framework;
using System.ComponentModel;

namespace Birkelund.QueryStrings.Test.IntegrationTests
{
	public class FullQueryStringEncryptionTest
	{
		[SetUp]
		public void Init()
		{
			//set default encryption before all tests start
			QueryStringConfiguration.Instance.SetFullEncryption();
		}

		[TearDown]
		public void Cleanup()
		{
			//remove encryption to ensure that the test leaves the environment in a clear state
			QueryStringConfiguration.Instance.RemoveFullEncryption();
			QueryStringConfiguration.Instance.ChecksumCalculator = ChecksumCalculators.Basic;
		}

		[Test]
		public void BestCase()
		{
			#region Setup variables
			Uri initialUrl = new Uri(@"http://localhost/test/testFile.aspx");

			IQueryStringToken<string> a = new StringToken("a");
			IQueryStringToken<DayOfWeek> b = new ValueTypeToken<DayOfWeek>("b");
			IQueryStringToken<TimeSpan> c = new ValueTypeToken<TimeSpan>("c");

			string valueA = "hello";
			DayOfWeek valueB = DayOfWeek.Thursday;
			TimeSpan valueC = TimeSpan.FromMilliseconds(765432);
			#endregion

			IUriWriter writer = UriWriter.CreateEmptyFromUri(initialUrl);
			writer.SetToken(a, valueA);
			writer.SetToken(b, valueB);
			writer.SetToken(c, valueC);

			string generatedString = writer.GenerateQueryAsString();
			StringAssert.DoesNotContain(valueA, generatedString); //ensures that encryption has taken place
			StringAssert.DoesNotContain(valueB.ToString(), generatedString);
			StringAssert.DoesNotContain(valueC.ToString(), generatedString);

			//verify that we are able to read encrypted values
			Uri generatedUri = writer.GenerateQueryAsUri();
			string actualA = a.ReadValue(generatedUri);
			Assert.AreEqual(valueA, actualA);

			DayOfWeek actualB = b.ReadValue(generatedUri);
			Assert.AreEqual(valueB, actualB);

			TimeSpan actualC = c.ReadValue(generatedUri);
			Assert.AreEqual(valueC, actualC);
		}

		[Test]
		public void EnsureDefaultValueIsReadIfDecryptionFailsInSilentMode()
		{
			IQueryStringToken<int> a = new ValueTypeToken<int>("a")
			{
				DefaultValue = int.MinValue,
				ExceptionNotificationBehaviour = ExceptionNotificationBehaviour.Silent
			};

			Uri initialUrl = new Uri(@"http://localhost/test/testFile.aspx?fe=invalid");

			int actualA = a.ReadValue(initialUrl);
			Assert.AreEqual(a.DefaultValue, actualA);
		}

		[Test]
		public void ReadEncryptedValuedInFullEncryptedContext()
		{
			IQueryStringToken<int> a = new ValueTypeToken<int>("a");
			IQueryStringToken<string> b = new StringToken("b") { EncryptionAlgorithm = new DesEncryptionImpl() };

			Uri initialUrl = new Uri(@"http://localhost/test/testFile.aspx?c=anything");
			IUriWriter writer = UriWriter.CreateFromUri(initialUrl);
			writer.SetToken(a, 123);
			writer.SetToken(b, "hello");

			//ensure that fullencryption has taken place
			string generatedUriString = writer.GenerateQueryAsString();
			StringAssert.DoesNotContain("123", generatedUriString);
			StringAssert.DoesNotContain("hello", generatedUriString);
			StringAssert.DoesNotContain("anything", generatedUriString);

			//encure that we can read the values properly again
			Uri generatedUri = writer.GenerateQueryAsUri();
			Assert.AreEqual(123, a.ReadValue(generatedUri));
			Assert.AreEqual("hello", b.ReadValue(generatedUri));
			//cannot read 'c' anymore as token is encrypted and decryption is only possible with a token-object

			QueryStringConfiguration.Instance.RemoveFullEncryption();
			//after removing full-encryption the generations should not be equal.

			string generatedUriStringNoFullEncryption = writer.GenerateQueryAsString();
			Console.Out.WriteLine(generatedUriStringNoFullEncryption);
			Assert.AreNotEqual(generatedUriString, generatedUriStringNoFullEncryption);

			StringAssert.Contains("123", generatedUriStringNoFullEncryption);
			StringAssert.DoesNotContain("hello", generatedUriStringNoFullEncryption); //this value is still encrypted
			StringAssert.Contains("anything", generatedUriStringNoFullEncryption);
		}

		[Test]
		public void FullEncryptionTokenDoesNotExist()
		{
			FailureTestHelper helper = new FailureTestHelper(FailureReason.FullEncryptionTokenDoesNotExist);

			Uri initialUrl = new Uri(@"http://localhost/test/testFile.aspx?a=hello");
			string actualA = helper.AToken.ReadValue(initialUrl);

			helper.VerifyDefaultValue(actualA);
		}

		[Test]
		public void FullEncryptionTokenIsNotTheOnlyToken()
		{
			FailureTestHelper helper = new FailureTestHelper(FailureReason.FullEncryptionTokenIsNotTheOnlyToken);

			Uri initialUrl = new Uri(@"http://localhost/test/testFile.aspx?fe=invalid&a=hello");
			string actualA = helper.AToken.ReadValue(initialUrl);

			helper.VerifyDefaultValue(actualA);
		}

		[Test]
		public void ModifiedQueryString_WithManipulation()
		{
			/*
			 * TestCase: 
			 * 1) Generate QS with full encryption
			 * 2) Make encrypted token invalid by manipulating it
			 * 3) read value
			 */
			FailureTestHelper helper = new FailureTestHelper(FailureReason.FullEncryptionError);

			Uri initialUrl = new Uri(@"http://localhost/test/testFile.aspx");
			IUriWriter writer = UriWriter.CreateEmptyFromUri(initialUrl);
			writer.SetToken(helper.AToken, "hello");

			UriBuilder builder = writer.GetUriBuilder();
			builder.Query = builder.Query.ToLower().Substring(1); //substring removes the ?-mark

			string actualA = helper.AToken.ReadValue(builder.Uri);

			helper.VerifyDefaultValue(actualA);
		}

		[Test]
		public void ModifiedQueryString_WithAppend()
		{
			/*
			 * TestCase: 
			 * 1) Generate QS with full encryption
			 * 2) Make encrypted token invalid by appending random text
			 * 3) read value
			 */
			FailureTestHelper helper = new FailureTestHelper(FailureReason.FullEncryptionError);

			IUriWriter writer = UriWriter.CreateEmptyFromUri(new Uri("http://localhost/test/testFile.aspx"));
			writer.SetToken(helper.AToken, "hello");

			string generatedString = writer.GenerateQueryAsString() + "NOTVALID"; //ensure that the encryption algorithm is not valid.
			string actualA = helper.AToken.ReadValue(new Uri(generatedString));

			helper.VerifyDefaultValue(actualA);
		}

		[Test]
		public void ReadEncryptedValueWithoutEncryption()
		{
			/*
			 * TestCase: 
			 * 1) Generate QS with full encryption
			 * 2) disable full encryption
			 * 3) read the values
			 */
			FailureTestHelper helper = new FailureTestHelper(FailureReason.TokenDoesNotExist);

			Uri initialUrl = new Uri(@"http://localhost/test/testFile.aspx");
			IUriWriter writer = UriWriter.CreateEmptyFromUri(initialUrl);
			writer.SetToken(helper.AToken, "hello");

			Uri generatedUri = writer.GenerateQueryAsUri();

			QueryStringConfiguration.Instance.RemoveFullEncryption();
			string actualA = helper.AToken.ReadValue(generatedUri);

			helper.VerifyDefaultValue(actualA);
		}

		[Test]
		public void EncryptionTokenDoesNotExist()
		{
			//fe token does not exist - and no other tokens are present}
			FailureTestHelper helper = new FailureTestHelper(FailureReason.FullEncryptionTokenDoesNotExist);

			Uri initialUri = new Uri("http://localhost/test/testFile.aspx");
			string actualA = helper.AToken.ReadValue(initialUri);

			helper.VerifyDefaultValue(actualA);
		}

		[TestCase(CalculatorType.Basic)]
		[TestCase(CalculatorType.Hash)]
		public void FullEncryptionCombinedWithCheckSum(CalculatorType calculator)
		{
			QueryStringConfiguration.Instance.ChecksumCalculator = TestUtil.GetStrategy(calculator);

			IQueryStringToken<int> a = new ValueTypeToken<int>("a");
			IQueryStringToken<int> b = new ValueTypeToken<int>("b");

			IUriWriter writer = UriWriter.CreateFromUri("http://www.test.com/default.aspx");
			writer.SetToken(a, 123);
			writer.SetToken(b, 456);
			writer.ApplyCheckSumControl = true;

			Uri generatedUri = writer.GenerateQueryAsUri();
			Assert.AreEqual(123, a.ReadValue(generatedUri, ReadValueMode.CheckSumControl));
		}

		/// <summary>
		/// Helper class to do repeatable 'plumber work'
		/// </summary>
		private class FailureTestHelper
		{
			private bool HasFailure { get; set; } //indicating if the reader actually failed
			private const string DefaultValue = "MyValue"; //the value we expect if readerror occours
			public IQueryStringToken<string> AToken { get; private set; }

			private readonly FailureReason _expected;
			private FailureReason? _actual = null;

			public FailureTestHelper(FailureReason expectedFailure)
			{
				_expected = expectedFailure;
				AToken = new StringToken("a")
				{
					ExceptionNotificationBehaviour = ExceptionNotificationBehaviour.Silent,
					DefaultValue = DefaultValue
				};

				AToken.ReadFailureAction = delegate(FailureArgs args)
				{
					_actual = args.Reason;
					if (args.Reason == expectedFailure)
					{
						HasFailure = true;
					}
				};
			}

			public void VerifyDefaultValue(string actualValue)
			{
				Assert.AreEqual(DefaultValue, actualValue);
				Assert.IsTrue(HasFailure, string.Format("The test expected: '{0}' but has value: '{1}'", _expected.ToString(), _actual.HasValue ? _actual.ToString() : "null"));
			}
		}
	}
}
